Technical Note TN1145
Living in a Dynamic TCP Environment

目次

このテクニカルノートでは、Mac OSが提供するダイナミックな環境下でTCP/IPを取り扱う際に発生する複雑な問題について説明します。特に、マルチホーミング(複数のIPアドレス)、ダイヤルアップリンク、PowerBook(および最近のデスクトップシステム)におけるスリープとスリープの解除、モデムの切断、ユーザ設定の変更などを正しく処理する方法を説明します。各々、BSDソケットとOpen Transportコードの書き方を解説します。

このテクニカルノートはMac OS Xや従来のMac OSのネットワークAPIを使用するデベロッパ向けに書かれています。

[2002年8月28日]






ダイナミックTCP/IP環境の基礎

多くのTCP/IPプログラムはTCP/IP環境について誤った前提に立っています。「このマシンはTCP/IPを使用しているので、IPアドレスを持っているにちがいない」、あるいは「IPアドレスは変更されないだろう」という前提は大学内のEthernetに接続されているワークステーションについては正しいかもしれませんが、Mac OSを実行し、PPPを介して接続されているPowerBookについては成立しません。このような誤った前提からすると、特に次の現象はMac OSのダイナミックTCP/IP環境から導き出される意外な結論といえます。

  1. IPアドレスを取得するまで、コンピュータがIPアドレスを持つことはありません。たとえば、コンピュータがPPPを介してアドレスを取得するように設定されている場合、モデムが接続されるまで、そのコンピュータがアドレスを取得することはありません。
  2. コンピュータのIPアドレスは時間の経過とともに変わることが場合があります。たとえば、コンピュータのDHCPリースが期限切れになった場合、TCP/IPスタックは新しいアドレスのネゴシエーションを行い、その結果、新しく取得されたアドレスが元のアドレスとは異なるものになることがあります。
  3. コンピュータが単一のIPアドレスを持つことや、すべてのIPアドレスは同等であると仮定すると、マルチホーミング(一台のコンピュータが複数のIPアドレスを持つ状態)と互換性を保つことができません。
  4. ユーザは、「TCP/IPアプリケーションを起動するときに自動的に接続する」ようにダイヤルアップリンク(Mac OS X PPP、ARA等)を設定することができます。アプリケーションが明示的なユーザリクエストなしにTCP/IPサービスを利用すると、予期せぬ状況でモデムがダイヤリングを開始するため、ユーザを混乱させてしまうことがあります。
  5. 従来のMac OS(Mac OS X以前)では、TCP/IPスタックがアンロードされると、TCP/IPプロバイダは自動的にクローズすることになります。

このテクニカルノートでは、これらの結論をうまく処理するためのハイレベルなアプローチを紹介します。主にMac OS Xを念頭に書かれていますが、従来のMac OSについても触れます。具体的には、マルチホーミングの注意事項ローカルIPアドレスリストの取得方法予期せぬ状況でモデムのダイヤリングを開始しないための注意事項ローカルIPアドレスリストの変更に対応する方法ループバックアドレスを使って通信する方法を説明します。



重要:
このテクニカルノートは次のネットワーキングAPIに焦点を絞っています。

  • Mac OS XのBSDソケットAPI
  • Mac OS XのOpen Transportコンパティビリティライブラリ
  • 従来のMac OSのOpen Transport API(Classic環境を含む)

MacTCPまたはOpen TransportのMacTCPインタフェース等の旧いAPIは取り扱いません。MacTCPプログラミングインタフェースは、このテクニカルノートで説明するすべてのケースを処理するには不十分です。

上記ネットワーキングAPIの上に位置するハイレベルAPIについても、このテクニカルノートでは取り扱いません。URL AccessやCFNetwork等のハイレベルAPIとダイナミックTCP/IP環境の関係については各APIの技術資料をご覧下さい。



用語集

Mac OSには、複数のネットワーキングAPIが存在します。ここでは、このテクニカルノートで使用する用語を明確に定義します。

従来のMac OS
Mac OS X以前のMac OSのすべてのバージョン。特定しない場合はClassic環境も含みます。
Mac OS X
Mac OS X 10.0以降。
Classic
Mac OS XのClassic互換性環境の中で実行する従来のMac OS。
Mac OS
従来のMac OSとMac OS Xを含む、Mac OSのすべてのバージョン。
MacTCP API
MacTCPとOpen Transportのみでサポートされている、非常に旧いプログラミングインターフェイス。
Open Transport API
Open Transportで導入されたXTIベースのTCP/IPプログラミングインターフェイス。Mac OS Xでは、このAPIはClassic環境およびCarbonベースのコンパティビリティライブラリで提供されています。
BSDソケット
Mac OS Xのネイティブ・ネットワーク・プログラミング・インターフェイス。UNIXの標準ネットワーキングAPIでもあります。世の中には、多数の参考書が存在しますので、これらを利用すると良いでしょう。いくつかは参考文献で紹介しています。
MacTCP
Open Transport登場以前の、従来のMac OSにおけるMacTCP APIプログラミングインターフェイス。
Open Transport
従来のMac OSおよびMac OS XのClassic環境のネットワーク環境。Open TransportはPower Macintosh 9500に搭載されていた漢字Talk 7.5.2で初めて登場しました。Open Transportはさかのぼって、漢字Talk 7.1までサポートしていました。Open Transportは漢字Talk 7.5.3で初めて標準インストールの対象となりました。Mac OS 7.6以降は、Open Transportがデフォルトのネットワーキングスタックとして搭載されました。
MacTCPコンパティブリティ
MacTCP APIを利用するアプリケーションの互換性を保つために、Open TransportはMacTCP APIの互換環境を提供していました。
BSDネットワーキング
Mac OS Xのコアネットワーキング環境。
Open Transportコンパティビリティライブラリ
Mac OS Xで、CarbonベースアプリケーションにOpen Transport APIを提供するために用意されている互換性ライブラリ。

共有ルーチン

このテクニカルノートで取り上げるコードサンプルの多くは、リスト1で紹介する共有ルーチンを利用しています。



リスト1 共有ルーチン

// エラー処理
// ----------
// SCFは2つの方法でエラーを返します:
//
// o 異常を示すために、関数結果が無効な値(NULLやfalseなど)と
//   なって返ってくることがあります。
//
// o 最後に実行した関数のエラーコードを返すためのルーチン
//   (SCError)が存在します。SCErrorの返すエラー番号は
//   OSStatusの範囲外のエラー番号となります。
//
// これをMoreSCErrorとMoreSCErrorBooleanの2つの関数で処理します。
// 2つの関数は一般的なエラー値(ポインタまたはBoolean)を受けて、
// エラーを示す内容であれば、SCErrorを呼び出して実際のエラー番号を
// 取得しています。同時に、SCエラー番号をOSStatusエラー番号の
// 有効範囲にマッピングするためのボトルネックの役割もはたすように。
// 設計されていますが、このサンプルではマッピングを省略しています。
//
// エラーパラメータの部分を省略して、単純にSCErrorを呼ぶことも
// 考えられますが、SCFがSCErrorを設定せずにエラーを返すことが心配
// でした。実際にそのようなことは報告例がないので余計な心配かもしれ
// ませんが、安全が第一です。

static OSStatus MoreSCErrorBoolean(Boolean success)
{
    OSStatus err;
    int scErr;

    err = noErr;
    if ( ! success ) {
        scErr = SCError();
        if (scErr == kSCStatusOK) {
            scErr = kSCStatusFailed;
        }
        // SCFエラーを直接OSStatusとして返しているが、これは真似しないで
        // 下さい。実際のプログラムでは、SCFエラー番号をOSStatusエラー番号の
        // 有効範囲にマッピングするなど、正しく対処して下さい。
        err = scErr;
    }
    return err;
}

static OSStatus MoreSCError(const void *value)
{
    return MoreSCErrorBoolean(value != NULL);
}

static OSStatus CFQError(CFTypeRef cf)
    // Core Foundationのエラー状況を(ありのままで)OSStatusドメインに
    // マッピングします。
{
    OSStatus err;

    err = noErr;
    if (cf == NULL) {
        err = coreFoundationUnknownErr;
    }
    return err;
}

static void CFQRelease(CFTypeRef cf)
    // NULLにも耐えられるCFRelease
{
    if (cf != NULL) {
        CFRelease(cf);
    }
}


トップに戻る



マルチホーミングの2つの注意事項

コンピュータが複数のIPアドレスを持つ場合、その状況をマルチホーミングと呼びます。ここでは、マルチホーミングで良く遭遇する2つの問題点と正しい処理方法について説明します。従来のMac OSとMac OS Xの両方で注意しなければなりません。



重要:
従来のMac OSの場合では、マルチホーミングはあまり関係ないと思うかもしれませんが、従来のMac OSでもマルチホーミングが有効となる場合があります。

  • Open Transport 1.3以降では、一つの物理インタフェースに複数のIPアドレスを設定することができます。(これをシングルリンク・マルチホーミングと呼びます。)
  • サードパーティソフトウェアを使用することによって、マルチリンク・マルチホーミングを使用することができます。
  • リモートアクセスのサーバが実行しているコンピュータがIPCP(PPP上のTCP/IP)経由で接続を受けると、そのコンピュータにはIPアドレスが1つ追加されます。

Mac OS Xを搭載しているコンピュータはすべてマルチホーミングをサポートします。ここに記載されている注意事項に気を付ければ、いずれのプラットフォームでもマルチホーミングをサポートすることができます。



マルチホーミングとバインド

一般的に言うと、マルチホーミングと互換性を保つためには、具体的なIPアドレスへのバインドを避けて、ワイルドカードIPアドレス(BSDソケットの場合はINADDR_ANY、Open Transportの場合はkOTAnyInetAddress)とバインドしなければなりません。外部と接続する場合のプログラミングは簡単です。BSDソケットの場合は単純にbindをせずに、直接connectを行います。Open Transportの場合はリスト2のようなコードになります。



リスト2 外部と接続する場合のコード

static OSStatus DoOutgoingBind(EndpointRef ep)
{
    OSStatus err;

    err = OTBind(ep, NULL, NULL);
    return err;
}




重要:
コンピュータの主要IPアドレスを取得して、そのアドレスにバインドすると、以下のことが言えます。

  • あなたは必要以上のコードを書いています。
  • マルチホーミングを使って、個別のインターネットに接続されているコンピュータの場合、あなたのコードは動作しません。
  • 将来IPv6への移行の際に、変更しなければならない箇所を1つ増やしていることになります。

リスト3では接続する場合の悪い例を紹介しています。





リスト3 以下のコードは外部と接続する場合の悪い例です。

// *** このようなコードを書かないで下さい! *** 

static OSStatus DoOutgoingBindWrongly(EndpointRef ep)
{
    OSStatus            err;
    InetInterfaceInfo   info;
    InetAddress         primaryAddr;
    TBind               bindReq;

    err = OTInetGetInterfaceInfo(&info, kDefaultInetInterface);
    if (err == noErr) {
        OTInitInetAddress(&primaryAddr, 0, info.fAddress);

        OTMemzero(&bindReq, sizeof(bindReq));
        bindReq.addr.buf = (UInt8 *) &primaryAddr;
        bindReq.addr.len = sizeof(primaryAddr);

        err = OTBind(ep, &bindReq, NULL);
    }
    return err;
}

// *** このようなコードを書かないで下さい! *** 


外部からの接続を受けるようなソフトウェアの場合も同じような状況です。基本的にはワイルドカードIPアドレス(INADDR_ANYまたはkOTAnyInetAddress)にバインドします。外部から接続を受ける場合はリスト4のようなコードになります。



リスト4 外部からの接続を受ける場合のコード(BSDソケットの場合とOpen Transportの場合)

static int DoIncomingBindBSDSockets(int sock, u_short port)
{
    int err;
    struct sockaddr_in req;

    memset(&req, 0, sizeof(req));
    req.sin_family = AF_INET;
    req.sin_addr.s_addr = htonl(INADDR_ANY);
    req.sin_port = htons(port);

    err = bind(sock, (struct sockaddr *) &req, sizeof(req));
    if (err == -1) {
        err = errno;
    }

    return err;
}

static OSStatus DoIncomingBindOT(EndpointRef ep, InetPort port)
{
    OSStatus    err;
    TBind       req;
    TBind       ret;
    InetAddress reqAddr;
    InetAddress retAddr;

    OTInitInetAddress(&reqAddr, port, kOTAnyInetAddress);

    OTMemzero(&req, sizeof(req));
    req.addr.buf    = (UInt8 *) &reqAddr;
    req.addr.len    = sizeof(reqAddr);
    req.qlen        = 10;

    OTMemzero(&ret, sizeof(ret));
    ret.addr.buf    = (UInt8 *) &retAddr;
    ret.addr.maxlen = sizeof(retAddr);

    err = OTBind(ep, &req, &ret);
    if (err == noErr) {
        if (retAddr.fPort != reqAddr.fPort) {
            (void) OTUnbind(ep);
            err = kOTAddressBusyErr;
        }
    }
    return err;
}


場合によっては、特定のIPアドレスにバインドすることが望ましいです。例えば、ウェブサーバはリクエストされたIPアドレスによって、異なる結果を返すかもしれません。DTSサンプルコードOTSimpleServerHTTPでは、この実現方法を紹介しています。しかし、IPアドレスによって異なる結果を返す場合以外は、上記のようにワイルドカードIPアドレスにバインドするのが正しいです。



註:
IPアドレスによって異なる結果を返す場合はサーバのエチケットについてもお読み下さい。



マルチホーミングとFTP

マルチリンク・マルチホーミングを行うように設定されているコンピュータでは、マシンの“正しい”IPアドレスがデータをやり取りするマシンのIPアドレスによって変わる可能性があります。

たとえば、2つのEthernetカードを搭載し、一方のカードがパブリックなインターネットに接続され、もう一方のカードがプライベートなネットワークに接続されているマシンがあるとします。それぞれのカードは個別のIPアドレスを持ちますが、このマシンがカード間でIPパケットをやり取りすることはありません。この状況は次のように図示できます。

Ethernetカードを2つ搭載したコンピュータ

図1 マルチホーミングの構成例

このとき、このコンピュータ上で実行されているFTPクライアントがあるとします。FTPプロトコルの(あまりありがたくない)仕様の1つは、データ接続 -data connection- をオープンするとき、FTPクライアントがローカルマシン上のポートをオープンし、そのポート番号とローカルIPアドレスをPORTコマンドでサーバに(制御接続 -control connection- を介して)送信しなければならないということです。ただし、パブリックなインターネット上のマシンは17.100.55.7のみを使って通信を行うことができ、プライベートネットワーク上のマシンは17.200.37.41のみを使って通信を行うことができます。このとき、FTPクライアントは送信するIPアドレスをどのようにして決定すればいいでしょうか?

その解答は、接続されているエンドポイントに対する正しいローカルIPアドレスを取得するためのAPIを使用することです。BSDソケットの場合はgetsockname、Open Transport APIの場合はOTGetProtAddressを呼び出します。つまり、FTPクライアントは制御接続上でこれらのAPIを呼び出して、データ接続に使用する正しいローカルアドレスを決定することができます。

リスト5では、BSDソケットやOpen Transportを使って、接続されているエンドポイントに基づいて送信すべき正しいローカルIPアドレスを見つけます。



リスト5 ローカルIPアドレスを取得します(BSDソケットの場合とOpen Transportの場合)

static int GetLocalIPAddressForConnectionBSDSockets(int sock,
                                    struct in_addr *localAddr)
{
    int err;
    int len;
    struct sockaddr_in local;

    len = sizeof(local);
    err = getsockname(sock, (struct sockaddr *) &local, &len);
    if (err == -1) {
        err = errno;
    }
    if (err == 0) {
        assert(len == sizeof(local));
        *localAddr = local.sin_addr;
    }

    return err;
}

static OSStatus GetLocalIPAddressForConnectionOT(EndpointRef ep,
                                         InetAddress *localAddr)
{
    OSStatus err;
    TBind localBind;

    // エンドポイントが接続済であることが前提です。
    assert(OTGetEndpointState(ep) == T_DATAXFER);
    assert(OTIsBlocking(ep));

    OTMemzero(&localBind, sizeof(TBind));
    localBind.addr.buf = (UInt8 *) localAddr;
    localBind.addr.maxlen = sizeof(InetAddress);

    err = OTGetProtAddress(ep, &localBind, NULL);
    return err;
}




註:
このルーチンではエンドポイントがT_DATAXFERステータスにあると断定しています。というのも、エンドポイントが接続されていない場合、OTGetProtAddressはエンドポイントがバインドされていたアドレスを返すだけで、これは一般に何の役にも立ちません。BSDソケットの場合は、getpeernameで同じテストが可能ですが、コードが多少複雑になります。





重要:
従来のMac OSにおけるOpen Transportアーキテクチャの制限により、上のコードによって返されるIPアドレスがすべてのIPアドレスのリストによって生成されたIPアドレスのリストに含まれていないこともあります。



トップに戻る



ローカルIPアドレスリスト

ここでは、マシンに割り当てられているIPアドレスのリストを取得する方法と、そのリスト内容が変わった時に通知を受ける方法を示します。



重要:
一般的には、ローカルIPアドレスに依存するようなプログラム設計は避けるべきです。これまで説明したような方法で実現可能です。コンピュータのIPアドレスを取得するのは以下の場合のみでしょう。

  • 外部からの接続があった時、IPアドレスによってふるまいが変わるようなサーバの場合。
  • IPアドレスのリストをユーザに提供する場合。

他の理由でIPアドレスを取得する場合は、マルチホーミングとの互換性を十分配慮するようなプログラム設計をする必要があります。

IPアドレスによってふるまいが変わるようなサーバの場合はサーバのエチケットについてもお読み下さい。



ローカルIPアドレスのリストを取得する

コンピュータのすべてのIPアドレス(ローカルIPアドレスのリスト)を取得する方法は、少なくても3通りあります。

  1. System Configurationフレームワーク
  2. Open Transport API
  3. BSDソケット

以下では、各々の方法について説明します。もっとも適切な方法は、必要とするシステム条件やコードの作り方によって異なります。

  • 従来のMac OSで動作する必要であれば、Open Transport APIを使用します。Mac OS Xでは、Open Transport APIのコードをそのまま流用するか、System Configurationフレームワークを利用するように書き換えます。
  • 他のUNIXプラットフォームと共有するコードは、BSDソケットを使用するのが適切です。
  • 動作条件がMac OS Xのみで、ローカルIPアドレスのリストが更新された時に通知が必要な場合は、System Configurationフレームワークを使用すると良いでしょう。
  • 選択の余地がある場合は、次の理由からSystem Configurationフレームワーク(SCF)を推奨します。
    • ローカルIPアドレスのリストが更新された時に通知を受けることができます。
    • ネットワークカーネルの仕様が変更されても、直接影響を受けることはありません。

System Configurationフレームワーク

System Configurationフレームワーク(SCF)は、Mac OS X 10.1以降でサポートされている、ネットワーク設定の取得/変更APIです。Mac OS Xでは、ローカルIPアドレスのリストを取得するためにSCFを利用します。リスト6を参照。



リスト6 System Configurationフレームワークを使って、ローカルIPアドレスのリストを取得する

static void GetIPAddressListFromValue(const void *key,
                                      const void *value,
                                      void *context)
    // この関数はCFDictionaryApplyFunctionを呼び出した時に、
    // CopyIPAddressListSCFのコールバック関数として利用
    // されます。ネットワークサービスCFDictionaryから
    // IPv4のアドレスリストを抽出して、contextポインタで
    // 渡されるCFDictionaryに追加します。
{
    CFArrayRef intfAddrList;

    assert( key != NULL );
    assert( CFGetTypeID(key) == CFStringGetTypeID() );
    assert( value != NULL );
    assert( CFGetTypeID(value) == CFDictionaryGetTypeID() );
    assert( context != NULL );
    assert( CFGetTypeID(context) == CFArrayGetTypeID() );

    intfAddrList = CFDictionaryGetValue(value,
                            kSCPropNetIPv4Addresses);
    if (intfAddrList != NULL) {
        assert( CFGetTypeID(intfAddrList)
                == CFArrayGetTypeID() );
        CFArrayAppendArray(context,
                            intfAddrList,
                            CFRangeMake(0, CFArrayGetCount(intfAddrList))
                            );
    }
}

static OSStatus CopyIPAddressListSCF(CFArrayRef *addrList)
    // このシステムが持つすべてのIPv4アドレスを配列(CFArrary)として
    // 返します。アドレスの順序に意味はなく、確約されていません。また、
    // アドレスはCFString形式として返ってきます。
{
    OSStatus            err;
    SCDynamicStoreRef   ref;
    CFStringRef         pattern;
    CFArrayRef          patternList;
    CFDictionaryRef     valueDict;
    CFMutableArrayRef   result;

    assert( addrList != NULL);
    assert(*addrList == NULL);

    ref         = NULL;
    pattern     = NULL;
    patternList = NULL;
    valueDict   = NULL;
    result      = NULL;

    // ダイナミックストアに接続して、「検索パターン」を指定する。
    // ここで指定するパターン「State:/Network/Service/[^/]+/IPv4」は
    // IPv4項目をすべて取得するためのパターンです。

    ref = SCDynamicStoreCreate( NULL,
                                CFSTR("CopyIPAddressListSCF"),
                                NULL,
                                NULL);
    err = MoreSCError(ref);
    if (err == noErr) {
        pattern = SCDynamicStoreKeyCreateNetworkServiceEntity(
                                NULL,
                                kSCDynamicStoreDomainState,
                                kSCCompAnyRegex,
                                kSCEntNetIPv4);
        err = MoreSCError(pattern);
    }

    // SCDynamicStoreCopyMultipleを呼び出して、パターンリストを作る。
    // SCDynamicStoreCopyValueではなく、SCDynamicStoreCopyMultipleを
    // 使う理由は、この瞬間を「スナップショット」として納めて、現在の状況を
    // 正確に保存できるためです。

    if (err == noErr) {
        patternList = CFArrayCreate(NULL,
                                    (const void **) &pattern,
                                    1,
                                    &kCFTypeArrayCallBacks);
        err = CFQError(patternList);
    }
    if (err == noErr) {
        valueDict = SCDynamicStoreCopyMultiple(ref,
                                               NULL,
                                               patternList);
        err = MoreSCError(valueDict);
    }

    // IPv4項目については、IPアドレスのリストを抽出して、
    // 結果を配列に追加する。

    if (err == noErr) {
        result = CFArrayCreateMutable(NULL, 0,
                                      &kCFTypeArrayCallBacks);
        err = CFQError(result);
    }

    // 各アドレスを配列に追加する。

    if (err == noErr) {
        CFDictionaryApplyFunction(valueDict,
                                  GetIPAddressListFromValue,
                                  result);
    }

    // 後処理

    CFQRelease(ref);
    CFQRelease(pattern);
    CFQRelease(patternList);
    if (err != noErr && result != NULL) {
        CFQRelease(result);
        result = NULL;
    }
    *addrList = result;

    assert( (err == noErr) == (*addrList != NULL) );

    return err;
}




註:
2002年8月28日現在、Mac OS X 10.2のSystem ConfigurationフレームワークはIPv6インターフェイスの情報を管理しません。(r. 2944943)



Open Transport API

リスト7のコードは、Open Transportの関数を使って、ローカルIPアドレスリストを取得しています。このコードは従来のMac OSとMac OS Xの両方で動作します。従来のMac OSで動作するソフトウェアを開発する場合は、この取得方法を推奨します。Mac OS Xで動作するソフトウェアを開発する場合は、この取得方法をそのまま採用しても構いませんが、可能な限りSystem Configurationフレームワークをお使い下さい。System Configurationフレームワークを使えば、リスト更新時に、変更通知を動的に受けられます。



リスト7 Open Transportを使って、ローカルIPアドレスのリストを取得する

typedef InetHost **InetHostHandle;

static OSStatus AddSecondaryAddresses(
                        InetInterfaceInfo* interfaceInfo,
                        SInt32 interfaceIndex,
                        InetHostHandle addrList)
{
    OSStatus err;
    InetHost *addrBuf;
    UInt32   addrCount;

    addrBuf = NULL;

    addrCount = interfaceInfo->fIPSecondaryCount;

    // 二次アドレス情報を受け取るためのバッファを用意。

    addrBuf = (InetHost *) NewPtr(addrCount * sizeof(InetHost));
    if (addrBuf == NULL) {
        err = kENOMEMErr;
    }

    // このインターフェイスの二次アドレスリストをOpen TransportにOTに
    // リクエストして、リストに加えます。

    if (err == noErr) {
        err = OTInetGetSecondaryAddresses(addrBuf,
                                          &addrCount,
                                          interfaceIndex);
    }
    if (err == noErr) {
        err = PtrAndHand(addrBuf,
                         (Handle) addrList,
                         addrCount * sizeof(InetHost));
    }

    // 後処理

    if (addrBuf != NULL) {
        DisposePtr( (Ptr) addrBuf );
    }

    return err;
}

enum
{
    kOTIPSingleLinkMultihomingVersion = 0x01300000
};

static OSStatus GetIPAddressListOT(InetHostHandle addrList)
{
    OSStatus            err;
    Boolean             haveIPSingleLinkMultihoming;
    NumVersionVariant   otVersion;
    SInt32              interfaceIndex;
    InetInterfaceInfo   info;
    Boolean             done;

    // システムタスク実行時であることが前提です。
    assert( TaskLevel() == 0 );

    SetHandleSize( (Handle) addrList, 0 );
    assert( MemError() == noErr );

    haveIPSingleLinkMultihoming =
        ( Gestalt(gestaltOpenTptVersions, (long *) &otVersion) == noErr
        && (otVersion.whole >= kOTIPSingleLinkMultihomingVersion )
        && ( OTInetGetSecondaryAddresses
                         != (void *) kUnresolvedCFragSymbolAddress));

    err = noErr;
    done = false;
    interfaceIndex = 0;
    do {
        done = ( OTInetGetInterfaceInfo(&info, interfaceIndex) != noErr );
        if ( ! done ) {

            // すべてのインターフェイスが使えない状態にあっても
            // Mac OS Xはデフォルトで、アドレスが「ゼロ」の
            // 単一インターフェイスを返しますので、その
            // インターフェイスはここで除外します。それ以外の
            // インターフェイスをはリストに追加します。

            if (info.fAddress != kOTAnyInetAddress) {
                err = PtrAndHand(&info.fAddress,
                                 (Handle) addrList,
                                 sizeof(InetHost));
            }

            // 二次アドレスを追加します。

            if (    err == noErr
                 && haveIPSingleLinkMultihoming
                 && info.fIPSecondaryCount > 0 ) {
                err = AddSecondaryAddresses(&info,
                                            interfaceIndex,
                                            addrList);
            }
            interfaceIndex += 1;
        }
    } while (err == noErr && !done);

    return err;
}




重要:
OTInetGetSecondaryAddressesはOpen Transport 1.3より前のバージョンでは実装されていません。Open Transport 1.3(Mac OS 8.1)より前のシステムとともにこのコードを正常に動作させるには、OpenTptInternetLibとの弱いリンクを行い、そのアドレスをkUnresolvedCFragSymbolAddressと比較して、それが存在するかどうかをチェックする必要があります。弱いリンクの詳細については、TN1083: Weak-Linking to a Code Fragment Manager-based Shared Libraryを参照してください。



BSDソケット

Mac OS X 10.2以降では、ネットワークインターフェイスと各々にアサインされているネットワークアドレスの一覧を返す関数(getifaddrs)が追加されています。BSD系のAPIでローカルIPアドレスリストを取得するには、この関数を使うことがもっとも簡単な方法です。しかし、getifaddrsはMac OS X 10.2以前のシステムでは存在しないので、その場合はもう少し掘り下げる必要があります。

BSDソケットAPIは、システムに存在するインターフェイスをすべてリストとして返すioctl(SIOCGIFCONF)を備えています。このioctlを使えば、ローカルIPアドレスリストが取得できます。使用例については、UNIX Network Programmingの434ページにあるget_ifi_infoを参照して下さい。この本のウェブサイトを参照すると、ソースコードもダウンロードできます。このioctlを使う方法は、Mac OS X 10.1.xまでのシステムや、他のUNIXプラットフォームへの移植が必要な時に有益です。

良いIPアドレスが悪いIPアドレスに転じる時

ローカルIPアドレスのリストをキャッシュしたり、間接的に保持(ローカルIPアドレスのリスナーを開くなど)すると、リストが更新された時に正しいアクションを取らなければなりません。具体的な方法はターゲットプラットフォームによります。Mac OS Xと従来のMac OSについては、推奨方法を以下で説明しています。両プラットフォームで動作するCarbonアプリケーションを開発している場合は、両方を手法を取り入れる必要があるでしょう。

Mac OS X

Mac OS Xにおいて、ローカルIPアドレスリスト更新時に通知を受け取るためには、System Configurationフレームワークにコールバック関数を登録します。登録方法はリスト8で示します。ここで紹介されている関数は、System Configurationフレームワークのダイナミックストアに接続して、該当するCFRunLoopを生成します。このrun loop sourceを自分のrun loop(通常はCFRunLoopGetCurrentで取得)に追加すると、System ConfigurationフレームワークダイナミックストアのIPv4項目が更新された時(つまり、ローカルIPアドレスリストが更新された時)、CreateIPAddressListChangeCallbackSCFに渡したコールバック関数が呼ばれます。



リスト8 System Configurationフレームワークを使って、IPアドレス更新時に通知を受ける。

static OSStatus CreateIPAddressListChangeCallbackSCF(
                        SCDynamicStoreCallBack callback,
                        void *contextPtr,
                        SCDynamicStoreRef *storeRef,
                        CFRunLoopSourceRef *sourceRef)
    // SCFダイナミックストアへの参照と、関連するCFRunLoopソースを
    // 生成します。ここで作られたCFRunLoopソースを自分のrun loopに
    // 追加すると、IPアドレスリストが更新される度に、
    // 指定されたコールバック関数が呼ばれます。
{
    OSStatus                err;
    SCDynamicStoreContext   context = {0, NULL, NULL, NULL, NULL};
    SCDynamicStoreRef       ref;
    CFStringRef             pattern;
    CFArrayRef              patternList;
    CFRunLoopSourceRef      rls;

    assert(callback   != NULL);
    assert( storeRef  != NULL);
    assert(*storeRef  == NULL);
    assert( sourceRef != NULL);
    assert(*sourceRef == NULL);

    ref = NULL;
    pattern = NULL;
    patternList = NULL;
    rls = NULL;

    // ダイナミックストアに接続して、「検索パターン」を指定する。
    // ここで指定するパターン「State:/Network/Service/[^/]+/IPv4」は
    // IPv4項目をすべて取得するためのパターンです。

    context.info = contextPtr;
    ref = SCDynamicStoreCreate( NULL,
                                CFSTR("AddIPAddressListChangeCallbackSCF"),
                                callback,
                                &context);
    err = MoreSCError(ref);
    if (err == noErr) {
        pattern = SCDynamicStoreKeyCreateNetworkServiceEntity(
                                NULL,
                                kSCDynamicStoreDomainState,
                                kSCCompAnyRegex,
                                kSCEntNetIPv4);
        err = MoreSCError(pattern);
    }

    // 1つのパターンのみのパターンリストを作成し、このパターンリストに
    // 該当するキーを監視したいことをSCFに伝える。そして、CFRunLoopの
    // ソースを生成します。

    if (err == noErr) {
        patternList = CFArrayCreate(NULL,
                                    (const void **) &pattern, 1,
                                    &kCFTypeArrayCallBacks);
        err = CFQError(patternList);
    }
    if (err == noErr) {
        err = MoreSCErrorBoolean(
                SCDynamicStoreSetNotificationKeys(
                    ref,
                    NULL,
                    patternList)
              );
    }
    if (err == noErr) {
        rls = SCDynamicStoreCreateRunLoopSource(NULL, ref, 0);
        err = MoreSCError(rls);
    }

    // Clean up.

    CFQRelease(pattern);
    CFQRelease(patternList);
    if (err != noErr) {
        CFQRelease(ref);
        ref = NULL;
    }
    *storeRef = ref;
    *sourceRef = rls;

    assert( (err == noErr) == (*storeRef  != NULL) );
    assert( (err == noErr) == (*sourceRef != NULL) );

    return err;
}


従来のMac OS

従来のMac OSで、ローカルIPアドレスリストの変更を知るには、Open Transportのバージョンによって手法が変わってきます。Open Transport 2.5以降(Mac OS 9以降に搭載)では、OTRegisterAsClientで登録した通知(Notifier)を受けることができます。具体的にはkOTStackWasLoadedイベントを受け取り、上記の説明に従ってローカルIPアドレスリストを構築します。

Open Transportの以前のバージョンでは、このようなイベントを受け取ることができません。唯一の確認方法は、ローカルIPアドレスリストを定期的ポーリングすることです。

トップに戻る



迷惑呼び出しの根絶

Mac OSでは、ネットワークコントロールパネルの「TCP/IPアプリケーションを起動するときに自動的に接続する」チェックボックスをクリックすることで、「ダイヤルオンデマンド」機能が働きます。しかし、モデムが予期しない状況でダイヤリングを行わないためには、ソフトウェア側の注意が必要です。具体的な対処方法はプラットフォームに依存しますが、まずはここで使う言葉を定義します。

用語集

このテクニカルノートでは、2種類のネットワークオペレーションを定義します。まず、「嘆願された」ネットワークオペレーションは、ユーザの直接的な指示によって実行されるものです。嘆願されたネットワークオペレーションはダイヤルオンデマンド機能を働かせ、必要に応じてモデムのダイヤリングを開始します。ユーザがウェブページの表示、メールの送受信、ファイルのダウンロードなどを直接指示した時は、モデムがダイヤリングを開始することを期待します。

一方では、「嘆願されていない」ネットワークオペレーションは、ユーザの間接的な指示によって実行されるものです。嘆願されていないネットワークオペレーションはダイヤルオンデマンド機能を働かせるべきではありません。例えば、ネットワーク経由のソフトウェアアップデート機能はこの分類に入ります。ソフトウェアアップデートの有無を確認するだけのためにモデムがダイヤリングを開始するべきではありません。

時々使われる手法としては、嘆願されていないネットワークオペレーションを「便乗的」に実行するアプローチです。ユーザが他の理由でネットワーク接続したすきに、接続に便乗してネットワークオペレーションを実行することができます。例えば、メールのサーバプログラムは、ネットワーク状況を常に把握して、ユーザがネットワークに接続した際に、便乗してメールの受信チェックなどを行うことができます。ネットワーク接続状況の確認方法は上記で説明しました。



註:
便乗的にネットワークを利用する場合は、長時間使用しないよう心掛けましょう。ネットワークトラフィックが発生しているかぎり、PPPの切断タイマーはリセットされず、ユーザが戸惑います。

ネットワーク経由のソフトウェアアップデート機能が良い例です。ソフトウェアアップデート機能は、ネットワーク接続のタイミンッグを見計らって、素早くソフトウェアアップデートの有無を確認します。しかし、ソフトウェアアップデートが存在する場合は、数メガバイトのファイルをダウンロードする前にユーザに通知をします。さもないと、予期しないタイミングでダウンロードのトラフィックが発生して、PPP接続の自動切断機能が働きません。



「嘆願された」オペレーションと「嘆願されていない」オペレーションの区別に悩むことは少なくありません。上記の例でも、メールサーバアプリケーションはネットワークを便乗的に使ってメールの受信チェックを行いますが、一定時間内(例えば24時間)にネットワーク接続がないと、メールの受信チェックを単独で行う方が良いかもしれません。最終的には、ソフトウェアの事情やユーザのフィードバックに応じて、プログラムを設計するケースが多々あるでしょう。究極的には、環境設定項目を設けることも考えられます。

以下で説明するテクニックは、いずれもモデムがコンピュータに直結している場合のみ有効です。あるコンピュータがAirMac ネットワークでAirMacベースステーションに接続され、そのベースステーションがダイヤルアップリンクでインターネットに接続されている場合は、ローカルコンピュータだけでは、リンクの状況が知り得ません。

Mac OS X

Mac OS Xでは「迷惑呼び出し」を避けるためのAPIが用意されています。このAPIを使えば、通信先のホストに対して、そのホストにアクセスする条件としてモデムをダイヤルする必要があるかどうか、事前に確認できます。「嘆願されていない」オペレーションを行う前に、System ConfigurationフレームワークのSCNetworkCheckReachabilityByAddressSCNetworkCheckReachabilityByNameを呼び出して、通信したいホストとの接続がモデムのダイヤルを伴うかどうか、確認して下さい。リスト9では、この確認方法を示しています。



リスト9 System Configurationフレームワークを使って、ネットワークの確認を行う

static Boolean UnsolicitedAllowedSCF(const char *serverName)
{
    Boolean                     result;
    SCNetworkConnectionFlags    flags;

    // 注意:
    // CodeWarriorで正しくコンパイルするためには、
    // 「enums are always int」オプションを「オン」にします。
    // CWPro8のMach-Oひな形では、この設定が「オフ」になっています。

    assert(sizeof(SCNetworkConnectionFlags) == sizeof(int));

    result = false;
    if ( SCNetworkCheckReachabilityByName(serverName, &flags) ) {
        result =    !(flags & kSCNetworkFlagsConnectionRequired)
                 &&  (flags & kSCNetworkFlagsReachable);
    }
    return result;
}


従来のMac OSとMac OS Xの大きな違いは、Mac OS Xの「ダイヤルオンデマンド」機能は実際にネットワークトラフィックを監視しているのに対し、従来のMac OSではTCP/IPプロバイダがオープンされることによってモデムがダイヤリングを行う点です。従って、Mac OS Xでは通信を行う前にネットワークの確認で十分なのに対し、従来のMac OSではTCP/IPプロバイダをオープンする前に確認する必要があります。以下では従来のMac OSについて、詳しく説明します。

従来のMac OS

従来のMac OSでは、実際にネットワークの使用が必要なときだけTCP/IPプロバイダを作成するように注意する必要があります。これは、TCP/IPプロバイダをオープンする動作によりTCP/IPスタックがロードされ、結果的に、ネットワークトラフィックをまったく送信しない場合でもモデムがダイヤリングを行うことになります。

ネットワークオペレーションを開始する前にOTInetGetInterfaceInfoを呼び出すだけで、アプリケーションは「期待されないオペレーション」としてのモデムのダイヤリングを避けることができます。TCP/IPスタックがロードされていることをOTInetGetInterfaceInfoが示す場合、モデム(接続されていれば)はすでにダイヤリングされており、アプリケーションは安全にネットワークを使用することができます。リスト10ではこの方法を示しています。



リスト10 OTInetGetInterfaceInfoを使って、接続状況を確認する

static Boolean UnsolicitedAllowedTrad(const char *serverName)
{
    #pragma unused(serverName)
    InetInterfaceInfo info;

    return ( OTInetGetInterfaceInfo(&info,
             kDefaultInetInterface) == noErr );
}


上記のコードは非常にシンプルですが、一部のデベロッパは堅牢性に欠けていると指摘しています。例えば、TCP/IPスタックはアンロードされるかもしれませんが、コンピュータはEthernet接続されているので、TCP/IPプロバイダを開いてもユーザは困らないでしょう。更に堅牢なコードについてはリスト11をご覧下さい。ここではDTSサンプルコード(MoreNetworkSetup)のNSHTCPWillDialが使われています。NSHTCPWillDialはより多くの条件下で動作しますが、コードもそれだけ複雑です。



リスト11 NSHTCPWillDialを使って、reachability(指定されたアドレスに辿り着けることができるかどうか)を確認します。

static Boolean UnsolicitedAllowedMNS(const char *serverName)
{
    #pragma unused(serverName)
    UInt32 willDial;

    return     (NSHTCPWillDial(&willDial) == noErr)
            && (willDial == kNSHTCPDialNo);
}




註:
TCP/IPスタックの生成方法の関係で、PPPリンクが接続されているにも関わらず、TCP/IPスタックがロードされていない場合があります。このような場合は、TCP/IPエンドポイントを開くのが安全であっても、NSHTCPWillDialはそのような結果を返しません。NSHTCPWillDialがこのような設計になっている理由は2つあります。

  • この状況は珍しいケースです。PPPの設定でダイヤルオンデマンド機能を設定するユーザが多いので、このようなユーザはPPPリンクが接続されていることはTCP/IPスタックがロードされていることを意味します。
  • PPPリンクの状況を確認するには、そのリンクに依存するコードが必要となります。良くあるリンク(ARA、FreePPP など)はコードを書けば済みますが、珍しいリンクや状況が把握しにくいリンク(AOLなど)をすべて対応することはほぼ不可能です。

NSHTCPWillDialは最悪でもfalse negativeを返すので、アルゴリズムが失敗する理由のうち、もっとも無難です。



トップに戻る



クローズを行うための準備

TCP/IPスタックは特定の条件が揃うと、予期しないタイミングで接続をクローズすることがあります。ここでは、接続がクローズされる条件について説明し、正しい対応方法を解説します。

Mac OS Xにおけるクローズを行うための準備

Mac OS Xでは、TCP/IPスタックは常にロードされた状態にあります。これによって、従来のMac OSで発生する問題の多くは解消されています。例えば、Mac OS Xでは、同じコンピュータにあるアプリケーション同士は常に通信することが可能(同一コンピュータ上でのデータのやり取りを参照)で、Open Transportのプロバイダがクローズされることもありません。しかし、ローカルIPアドレスリスト更新時には、接続元のIPアドレスが無効接続となることがあります。この場合は、成立していた接続が事実上無効となり、通信が不可能になります。アプリケーション開発者は、例えばOpen TransportエンドポイントならT_DISCONNECTイベントが発生するなどの通知を期待するかもしれませんが、残念ながらこのような事態においては、アプリケーションが通知を受けることはありません。むしろ、接続は存在したままで、通信のみが不可能となります。

これはTCP/IPプロトコル自体が関係しているので、BSDソケットも、Open Transportエンドポイントも、同じ問題を抱えています。ローカルIPアドレスリストからアドレスが消えた時に、そのローカルアドレスを使っていた接続は無効となります。このように無効接続に遭遇する可能性があるのは主に次の通りです。

  • 接続されているTCPソケットとエンドポイント
  • 接続されているUDPソケット
  • 接続されていないTCPソケット、UDPソケット、そして特定のIPアドレス(ワイルドカードIPアドレス以外)にバインドされているソケットやエンドポイント

無効接続への対処方法は、アプリケーションの性質によって異なります。通常のクライアント型アプリケーションは接続/通信時間が限られるので、特別な対策は必要ないでしょう。このようなアプリケーションは、標準的なタイムアウトの仕組みを持っていることがほとんどなので、その仕組みが正しく働けば、無効接続にも対応できるでしょう。

一方、長期に接続を維持するようなクライアントアプリケーションは、何らかの措置をとった方が良いでしょう。1つの方法は、上記で説明したように、System Configurationフレームワークを使って、ローカルIPアドレスリストを監視する方法です。ローカルIPアドレスリストの変更を感知すると、接続中のローカルアドレス(こちらも上記で説明した方法で取得)が消えていないか確認します。接続中のローカルアドレスがローカルIPアドレスリストから消えている場合は無効接続を意味するので、迷惑呼び出しを配慮しながら、接続を再構築します。リスト12では、BSDソケットとOpen Transport API各々で無効接続を感知する方法を紹介しています。



注意:
インターフェイスが一時的に無効となること(例えば、一時的にAirMacネットワークの圏外に移動した場合や、ハブの接続ポートを変更した場合など)は沢山あるので、インターフェイスが無効となってから接続を終了するまでの間に、間隔を設けることが重要です。





リスト12 接続が無効となっているかどうかを確認する。

static Boolean IsEndpointStale(EndpointRef ep,
                               InetHostHandle addrList)
{
    Boolean     stale;
    InetAddress sourceAddr;
    ItemCount   index;
    ItemCount   count;

    if ( GetLocalIPAddressForConnectionOT(ep,
                                    &sourceAddr) != noErr ) {
        stale = true;
    } else {
        assert(sourceAddr.fAddressType == AF_INET);

        if (sourceAddr.fHost == kOTAnyInetAddress) {
            stale = false;
        } else {
            stale = true;
            count = GetHandleSize( (Handle) addrList ) / sizeof(InetHost);
            for (index = 0; index < count; index++) {
                if ( sourceAddr.fHost == (*addrList)[index] ) {
                    stale = false;
                    break;
                }
            }
        }
    }

    return stale;
}

static Boolean IsSocketStale(int sock, CFArrayRef addrList)
{
    Boolean         stale;
    struct in_addr  sourceAddr;
    CFStringRef     sourceAddrStr;

    sourceAddrStr = NULL;

    if ( GetLocalIPAddressForConnectionBSDSockets(
                                        sock,
                                        &sourceAddr) != 0 ) {
        stale = true;
    } else {
        if (sourceAddr.s_addr == INADDR_ANY) {
            stale = false;
        } else {
            stale = true;

            sourceAddrStr = CFStringCreateWithCString(
                                        NULL,
                                        inet_ntoa(sourceAddr),
                                        kCFStringEncodingASCII);
            if (sourceAddrStr != NULL) {
                stale = ! CFArrayContainsValue(
                                 addrList,
                                 CFRangeMake(0, CFArrayGetCount(addrList)),
                                 sourceAddrStr);
            }
        }
    }

    CFQRelease(sourceAddrStr);

    return stale;
}


サーバアプリケーションの場合は、サーバのエチケットに従って下さい。



注意:
従来のMac OSは以下で説明するように、ローカルIPアドレスリストが変更される度に、すべてのTCP/IPプロバイダを閉じることによって、不正な接続が残ろことを防いでいます。今日のMac OS Xはこのような制御をしないので、kOTProviderWillClosekOTProviderHasClosedメッセージをOpen Transportコールバック関数に送ることはありません。しかし、将来はMac OS XのOpen Transport互換性ライブラリにこのような機能が追加される可能性がありますので、このようなイベントに正しく対応しなければなりません。ほとんどの場合は、これらの2つのイベントを無視して、上記のように不正が接続が残らないような工夫をすれば十分なはずです。



クローズを行うための準備

従来のMac OSでは、TCP/IPスタックはもっと複雑な歩みをします。Open Transportの重要な要素として、TCP/IPスタックのロード/アンロード機能があります。スタックがロードされていないと、TCP/IP関連のサービスは利用できません。TCP/IPプロバイダを生成すると、Open TransportはTCP/IPスタックを必要に応じてロードします。コンピュータのダイヤルオンデマンド機能が働いている場合は、TCP/IPスタックをロードすることで、モデムがダイアルを開始します。モデム接続が失敗すると、TCP/IPスタックのロードも失敗しますので、プロバイダを生成したアプリケーションに対して、プロバイダのクリエータ関数がエラーを返します。

予期せぬ状況でモデムのダイヤリングを開始しないための注意事項についてもご覧下さい。

TCP/IPスタックは次の条件が満たされるとアンロードされることがあります。

  • TCP/IPがモデムを使用している時にモデムが切断された場合
  • マシンのIPアドレスが変更された場合(例えば、DHCPのリース期限が切れて、新しいアドレスを取得できなった場合)
  • マシンがスリープした場合(ただし、以下の例外「註」を参照)
  • ユーザがTCP/IPコントロールパネルの設定を変更した場合
  • アプリケーションがネットワーク設定APIを使って、ネットワーク設定を変更した場合
  • すべてのプロバイダがクローズされてから2分以上経過した場合(Open Transportの初期バージョンのみ)


註:
TCP/IPスタックは、「ディープスリープ」時のみにアンロードされます。これはPowerBook、Power Mac G4(AGP Graphics)以降、iMac DV以降で発生します。また、「ネットワーク管理者のアクセスによってスリープを解除する」オプションに対応するコンピュータの場合は、この設定が「入」であればスリープ時にTCP/IPスタックがアンロードされることはありません。



通常のオペレーションの一部として、TCP/IPスタックがプロバイダのクローズを決定する場合があります。TCP/IPスタックがプロバイダをクローズするとき、Open Transportは2つのイベントのいずれかを使ってプロバイダの通知子(Notifier)を呼び出します。

  1. kOTProviderWillCloseは、OTが制御された方法でプロバイダをクローズしているとき、通常は、ユーザがTCP/IPスタックを再設定しているときに送信されます。これは常にシステムタスクの実行時に送信されます。プロバイダを同期モードに置き、ネットワーク接続の完全なシャットダウンを選択してもかまいません。
  2. kOTProviderHasClosedは、OTがプロバイダを「強制的にクローズ」するときに送信されます。通常、これは下層のリンク層がシャットダウンされたときに実行されます。kOTProviderHasClosedはシステムタスクまたは遅延タスクの実行時に送信されます。このため、通常はそれが遅延タスクの実行時に送信されるという前提に立ってコードを書きます。下層のプロバイダがすでにクローズされていても、OTCloseProviderを呼び出して、(小さな) メモリリークを避ける必要があります。

通知子の動作に関係なく、通知子が制御を返すときにプロバイダは常にクローズされます。通知子が制御を返した後にプロバイダで実行されたオペレーションはすべてkOTBadReferenceErrを返すことになります。

アクティブな作業を行っているTCP/IPプロバイダでは (たとえば、アクティブにデータを転送しているワーカエンドポイント)、これらのイベントを無視することができます。次にこのプロバイダを使用するとkOTBadReferenceErrエラーが返されますが、プロバイダをクローズすることでジェネリックなエラー処理がこのエラーに応答できるはずです。しかし、アクティブな作業を行っていないTCP/IPプロバイダ (定期的なDNSの検索に使用するInetSvcRefやサーバ内のリスニングエンドポイントなど) をオープンする場合は、これらのイベントを処理する通知子をインストールする必要があります。リスト13はその例です。



リスト13 プロバイダのクローズイベントの処理

static InetSvcRef gInetServices = kOTInvalidProviderRef;

static pascal void MyInetServicesNotifier(
        void* contextPtr,
        OTEventCode code,
        OTResult result,
        void* cookie)
{
    #pragma unused(contextPtr)
    #pragma unused(result)
    #pragma unused(cookie)
    switch (code) {
        case kOTSyncIdleEvent:
            // [... Yieldをするコードは外しました ...]
            break;

        case kOTProviderWillClose:
        case kOTProviderIsClosed:
            // OTはプロバイダをクローズしています。
            // 次に誰かがMyStringToAddressを呼び出すときにプロバイダを
            // 再オープンできるように、ここではプロバイダへの参照を削除します。
            (void) OTCloseProvider(gInetServices);
            gInetServices = kOTInvalidProviderRef;
            break;
        default:
            // 何もしません。
            break;
    }
}

static OSStatus MyOpenInetServices(void)
{
    OSStatus err;
    OSStatus junk;

    gInetServices = OTOpenInternetServicesInContext(
        kDefaultInternetServicesPath, 0, &err, NULL);
    if (err == noErr) {
        junk = OTSetBlocking(gInetServices);
        assert(junk == noErr);
        junk = OTSetSynchronous(gInetServices);
        assert(junk == noErr);
        junk = OTInstallNotifier(gInetServices,
            MyInetServicesNotifier, NULL);
        junk = OTUseSyncIdleEvents(gInetServices, true);
        assert(junk == noErr);
    }
    return err;
}

static OSStatus MyStringToAddress(const char *string,
        InetHost *address)
{
    OSStatus err;
    InetHostInfo hostInfo;

    // DNSプロバイダが現在オープンされていない場合はオープンします。

    err = noErr;
    if (gInetServices == kOTInvalidProviderRef) {
        err = MyOpenInetServices();
    }

    // ここではプロバイダを使って名前からアドレスへの変換を行います。

    if (err == noErr) {
        err = OTInetStringToAddress(gInetServices,
                 (char *) string, &hostInfo);
    }
    if (err == noErr) {
        // この例ではホストの1つ目のIPアドレスを取得します。
        *address = hostInfo.addrs[0];
    }
    return err;
}


リスト13は2つの重要な点を具体的に示しています。

  1. gInetServicesプロバイダは、プログラムが名前からアドレスへの変換を必要とするまで作成されません。この種の「ゆっくり」としたプロバイダの作成により、プログラムが時期尚早にモデムをダイヤリングするのを防ぐことができます。
  2. プロバイダがクローズしていることが知らされると、通知子はgInetServicesを無効にします。次にMyStringToAddressが呼び出されると、通知子はこのことを通知し、プロバイダを再作成します。

Open Transportは、ローカルIPアドレスのリストが変更されるタイミングで、プロバイダを必ずクローズします。従って、従来のMac OSでは、無効な接続が存在することはあり得ません。

トップに戻る



サーバのエチケット

上記で説明したイベントを適切に処理することは、サーバにとって特に大事です。サーバは一般的に、複数のlistener(BSDソケットの場合はlistenerソケット、Open Transportの場合はlistenerエンドポイント)が開いた状態でクライアント接続を待っています。ネットワーク設定が変更されても、これらのlistenerが有効であることが大切です。

Mac OS Xにおけるリスナ

Mac OS Xで上記のアドバイスに従うと、listenerの処理は非常に簡単です。Mac OS XのTCP/IPスタックがアンロードされることはないので、listenerがクローズされてしまう心配はありません。また、listenerはワイルドカードIPアドレス(BSDソケットの場合はINADDR_ANY、Open Transportの場合はkOTAnyInetAddress)にバインドされているので、ローカルIPアドレスのリストが変わっても、listenerが無効になることもありません。

特定のIPアドレスにlistenerをバインドしている場合(クライアントが指定したIPアドレスに応じて異なる結果を返すような場合)はローカルIPアドレスのリストを常に監視しなければなりません。ローカルIPアドレスのリストからIPアドレスが消えた場合は、そのIPアドレスに相当するlistenerをクローズしなければなりません。ローカルIPアドレスのリストに新しいIPアドレスが現れた場合は、そのIPアドレスに対して新しいlistenerをオープンします。よく使われる手法は、ローカルIPアドレスの変更を察知した時点ですべてのlistenerをクローズして、変更後のIPアドレスリストに応じて新しいlistenerをオープンします。

上記はBSDソケットを使っている場合も、Open Transportを使っている場合も有効と言えます。

従来のMac OSにおけるリスナ

サーバがリスニングエンドポイント (「リスナ」) をオープンし、TCP/IPスタックがアンロードされると、これらのリスナはクローズし、その結果、通知子はそれに対応するイベントを受け取ります。このイベントを適切に処理できなくても、サーバはおそらく外見上正常に動作し続けます。ただし、サーバーはそれ以上T_LISTENイベントを受け取らなくなり、クライアントの声が「聞こえなく」なるはずです。

リスナのクローズに対する標準的な応答は、単純にリスナを再オープンすることです。ただし、通知子の中で直接これを行おうとしてはいけません。むしろ、TCP/IPがロードされるとすぐに、メインイベントループにリスナを再オープンさせるようなフラグを設定してください。迷惑呼び出しの根絶で説明したように、OTInetGetInterfaceInfoの実行結果をチェックすることで、このことを判断できます。

通常、リスナのオープンは期待されないオペレーションであり、それがモデムをダイヤリングする原因になる場合は遅延を行う必要があります。アプリケーションがモデムをダイヤリングしてリスナをオープンする環境で使用される可能性がある場合は、それに対応するユーザー設定を提供することをお勧めします。次に、その例を示します。通常は最初のオプションをデフォルトにします。

サーバプログラムのモデム設定のラジオボタンの例

図2 サーバプログラムのモデム設定の例

トップに戻る



同一コンピュータ上でのデータのやり取り

独り言は狂気の第1歩かもしれませんが、TCP/IPソフトウェアは同じマシンで実行されている他のソフトウェアとしばしばデータのやり取りを行います。たとえば、ネットワークを介してサーバの環境設定を行うサーバ管理ツールがあるとします。ユーザがサーバと管理ツールを同じマシン上で実行することはごく当たり前のことです。

従来のMac OSやMac OS Xでは、標準的な127.0.0.1ループバックアドレスを含めて、この種のループバック機構がサポートされています。ただし、システムのバージョンによっては制限もあります。

Mac OS Xにおけるループバックサポート

Mac OS XのTCP/IPスタックは常にロードされているので、ループバックアドレスは常に有効です。Mac OS Xでは、ループバックアドレスを使用することによって、モデムがダイヤリングを開始する心配はありません。

Mac OS Xにおけるループバックサポートの唯一の制限はClassic環境にあります。ループバックアドレス127.0.0.1は非Classic環境(Carbon、Cocoa、BSD、Java)のプロセス間で有効です。また、ループバックアドレスはClassic環境のプロセス間でも有効です。しかし、非Classic環境とClassic環境間の通信はできません。例えば、Classic環境で動作するサーバプログラムは、ループバックアドレス127.0.0.1を使って、非Classicプログラムからアクセスすることができません。

ワークアラウンドとしては、127.0.0.1のかわりに共有IPアドレスを直接使うことです。ただし、共有IPアドレスを取得する場合は、このテクニカルノートの最初の方で触れた問題点もあります。



重要:
Mac OS X 10.1〜10.1.4のバグにより、Classicプログラムと非Classicプログラム間のループバックは正常に動作しませんでした。Mac OS X 10.1.5やMac OS X 10.2以降では、Classicプログラムと非Classicプログラム間のループバックは正常に動作するようになりました。



従来のMac OSにおけるループバックサポート

従来のMac OSにおけるループバックサポートはMac OS Xより複雑です。ダイヤルアップ接続として設定されたマシンで問題が発生します。TCP/IPプロバイダのオープンはTCP/IPスタックをロードすることになり、TCP/IPスタックのロードはモデムをダイヤリングする原因になることを思い出してください。このことは、エンドポイントを使用して同一コンピュータと通信する場合にも当てはまります。残念ながら良い回避方法はありません。

理想とはほど遠い対処方法は、モデム経由で接続を行わないようにTCP/IPを再設定することです。その他の適切なリンクが使用可能でない場合は、TCP/IPを“MacIP”(設定方法は“MacIPを手入力”)に設定し、AppleTalkを“リモートのみ”に設定します。図3では、これらの設定を行ったダイアログボックスを示します。

TCP/IPコントロールパネル

AppleTalkコントロールパネル

図3 Open Transportのループバック用の設定

Network Setupライブラリを使用すると、プログラムを使ってこれらの設定を作成して設定することができます。

トップに戻る



要約

TCP/IPの使用はもはやEthernetに接続されたデスクトップマシンに制限されません。TCP/IPアプリケーションはTCP/IP環境に関する誤った前提に立つことなく、ユーザによるコンピュータの再設定や設置場所の移動といったTCP/IP環境の劇的な変化に対応していなければなりません。また、予期せぬモデムのダイヤリングによってダイヤルアップ接続を使用するユーザに混乱を与えないように努力する必要があります。このテクニカルノートの情報はダイナミックTCP/IP環境での使用を目的としたソフトウェアを開発する上で参考になるはずです。

トップに戻る



参考文献

Apple, Inside Macintosh: Networking with Open Transport, Apple, 1997

Apple, Inside Macintosh: Network Setup, Apple, 2000

Apple, Inside Mac OS X: System Configuration Framework Overview, Apple, 2002

J Postel, J Reynolds, Internet RFC 959: File Transfer Protocol (FTP), IETF Network Working Group, 1985

W Richard Stevens, UNIX Network Programming: Networking APIs: Sockets and XTI, Prentice Hall, 1998, ISBN 013490012X

DTS テクニカルノート 1083 Weak-Linking to a Code Fragment Manager-based Shared Library

DTS テクニカルノート 1121 Mac OS 8.1

DTS テクニカルノート 1176 Mac OS 9

DTS サンプルコード OTSimpleServerHTTP

DTS サンプルコード MoreSCF

DTS サンプルコード MoreNetworkSetup

トップに戻る



変更歴

1998年11月1日

初版

2000年1月2日

PrintIPAddressesサンプルコードの修正、OTTCPWillDialのかわりにMoreNetworkSetupを参照し、MoreNetworkSetupの迷惑呼び出しの根絶の境界線について説明を加えて、Open Transport 2.5で加わったTCP/IPスタックロードの通知に関する説明を加えました。

2000年11月17日

IPアドレスに関する説明を更に詳しくし、エンドポイントのバインド方法を具体的に示しました。

2002年8月22日

BSDソケット、Open Transportコンパティビリティライブラリ、System ConfigurationフレームワークなどのMac OS X関連の話題を追加し、大幅に変更しました。


トップに戻る



ダウンロード

Acrobat

MoreNetworkSetup(164K)

ダウンロード




トップに戻る